using System;
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
using System.IO;
using System.Reflection;
using Accountability4net;
using Accountability4net.Contact;
using System.Drawing;
using System.Drawing.Imaging;

namespace Accountability4net.UnitTests
{
    [TestFixture]
    public class ProviderTests
    {
        [TestFixtureSetUp]
        public void TestFixtureSetUp()
        {
        }

        [SetUp]
        public void TestSetup()
        {
            // Reset the file before running test
            string sourceCodeBase = new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath;
            string sourceCodeBaseDirectory = Path.GetDirectoryName(sourceCodeBase);
            string sourceFilePath = Path.Combine(sourceCodeBaseDirectory, "clean.monengine4net.xml");

            string targetCodeBase = new Uri(typeof(FileMonengineProvider.FileMonengineProvider).Assembly.CodeBase).LocalPath;
            string targetCodeBaseDirectory = Path.GetDirectoryName(targetCodeBase);
            string targetFilePath = Path.Combine(targetCodeBaseDirectory, "monengine4net.xml");
            File.Copy(sourceFilePath, targetFilePath, true);

            foreach (MonengineProviderBase provider in MonengineManager.Providers)
            {
                provider.Install();
            }
        }

        [TearDown]
        public void TearDown()
        {
            foreach (MonengineProviderBase provider in MonengineManager.Providers)
            {
                provider.Dispose();
            }
        }

        [Test]
        public void Providers_Instantiate()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    Assert.IsTrue(provider.Name.EndsWith("Provider"));
                }
            );
        }

        [Test]
        public void Providers_CreateAccount()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    IAccount account = provider.CreateAccount(11, AccountType.ASSET);
                    Assert.AreEqual(0, account.Balance);
                }
            );
        }

        [Test]
        public void Providers_CreateAction()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    IAccount account = provider.CreateAccount(11, AccountType.ASSET);
                    IAction action = provider.CreateAction(ActionType.CREDIT, account, new Decimal(10));
                }
            );
        }

        [Test]
        public void Providers_CreateTransaction()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    // Setup data
                    IAccount bankAccount1 = provider.CreateAccount(11, AccountType.ASSET);
                    IAccount creditorAccount2 = provider.CreateAccount(12, AccountType.LIABILITY);
                    IAccount incomeAccount3 = provider.CreateAccount(13, AccountType.INCOME);
                    IAccount expenseAccount4 = provider.CreateAccount(14, AccountType.EXPENSE);

                    // Perform actions
                    // First initialize the asset and liability accounts
                    // Deposit 100.25 in the bank account
                    bankAccount1.Debit(new Decimal(100.25), null);
                    Assert.AreEqual(100.25, bankAccount1.Balance);

                    // We owe creditor 50.75 say
                    creditorAccount2.Credit(new Decimal(50.75), null);
                    Assert.AreEqual(50.75, creditorAccount2.Balance);

                    // Now inside a transaction we make 20.95 and deposit that to our bank account
                    ITransaction transaction = provider.CreateTransaction(DateTimeOffset.Now);
                    transaction.Add(provider.CreateAction(ActionType.CREDIT, incomeAccount3, new Decimal(20.95)));
                    transaction.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    Assert.AreEqual(0, incomeAccount3.Balance);
                    Assert.AreEqual(100.25, bankAccount1.Balance);

                    transaction.Commit();
                    Assert.AreEqual(20.95, incomeAccount3.Balance);
                    Assert.AreEqual(121.20, bankAccount1.Balance);
                }
            );
        }


        [Test]
        public void Providers_Commit_Unbalanced_Transaction()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    // Setup data
                    IAccount bankAccount1 = provider.CreateAccount(11, AccountType.ASSET);
                    IAccount creditorAccount2 = provider.CreateAccount(12, AccountType.LIABILITY);
                    ITransaction transaction = provider.CreateTransaction(DateTimeOffset.Now);
                    transaction.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount2, new Decimal(20.951)));

                    // Perform actions
                    try
                    {
                        transaction.Commit();
                        Assert.Fail("Expected UnbalancedTransactionException");
                    }
                    catch (UnbalancedTransactionException)
                    {

                    }
                }
            );
        }

        [Test]
        public void Providers_Get_Existing_Account()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    IAccount bankAccount1 = provider.CreateAccount(11, AccountType.ASSET);
                    bankAccount1.Name = "Inventory";
                    bankAccount1.Description = "Items on stock";
                    IAccount bankAccount2 = provider.GetAccount(11);
                    Assert.AreEqual(bankAccount1 ,bankAccount2);
                }
            );
        }

        [Test]
        public void Providers_Get_Accounts()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    IList<IAccount> existingAccounts = provider.GetAccounts();
                    IAccount bankAccount1 = provider.CreateAccount(511, AccountType.ASSET);
                    IAccount bankAccount2 = provider.GetAccount(511);
                    IList<IAccount> accounts = provider.GetAccounts();
                    Assert.AreEqual(existingAccounts.Count + 1, accounts.Count);
                }
            );
        }

        [Test]
        public void Providers_Get_Accounts_Limit()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    IAccount bankAccount1 = provider.CreateAccount(11, AccountType.ASSET);
                    IAccount bankAccount2 = provider.CreateAccount(12, AccountType.ASSET);
                    IAccount bankAccount3 = provider.CreateAccount(13, AccountType.ASSET);
                    IAccount bankAccount4 = provider.CreateAccount(14, AccountType.ASSET);
                    IList<IAccount> accounts = provider.GetAccounts(11, 2);
                    Assert.AreEqual(2, accounts.Count);
                }
            );
        }

        [Test]
        public void Providers_GetTransactions_All()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    int initial = provider.GetTransactions().Count;
                    // Setup data
                    IAccount bankAccount1 = provider.CreateAccount(11, AccountType.ASSET);
                    IAccount creditorAccount2 = provider.CreateAccount(12, AccountType.LIABILITY);
                    ITransaction transaction = provider.CreateTransaction(DateTimeOffset.Now);
                    transaction.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount2, new Decimal(20.95)));
                    ITransaction transaction2 = provider.CreateTransaction(DateTimeOffset.Now);

                    IList<ITransaction> transactions = provider.GetTransactions();
                    Assert.AreEqual(initial, transactions.Count);
                    transaction.Commit();
                    transactions = provider.GetTransactions();
                    Assert.AreEqual(initial+1, transactions.Count);
                    transaction2.Commit();
                    transactions = provider.GetTransactions();
                    Assert.AreEqual(initial+2, transactions.Count);
                }
            );
        }

        [Test]
        public void Providers_GetTransactions_For_Account()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    // Setup data
                    IAccount bankAccount1 = provider.CreateAccount(11, AccountType.ASSET);
                    IAccount creditorAccount2 = provider.CreateAccount(12, AccountType.LIABILITY);
                    IAccount creditorAccount3 = provider.CreateAccount(13, AccountType.LIABILITY);
                    ITransaction transaction = provider.CreateTransaction(DateTimeOffset.Now);
                    transaction.Description = "First transaction";
                    transaction.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount2, new Decimal(20.95)));
                    transaction.Commit();
                    ITransaction transaction2 = provider.CreateTransaction(DateTimeOffset.Now);
                    transaction2.Description = "Second transaction";
                    transaction2.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction2.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount3, new Decimal(20.95)));
                    transaction2.Commit();

                    List<ITransaction> transactions = new List<ITransaction>(provider.GetTransactions(creditorAccount3));
                    Assert.AreEqual(1, transactions.Count);
                    Assert.AreEqual("Second transaction", transactions[0].Description);
                    Assert.IsTrue(transactions.Exists(
                            delegate(ITransaction trans)
                            {
                                List<IAction> actions = new List<IAction>(trans);
                                List<IAction> accountActions = actions.FindAll(
                                    delegate(IAction action)
                                    {
                                        return action.Account.Number == creditorAccount3.Number;
                                    }
                                );
                                return accountActions.Count == 1;
                            }
                        )
                    );
                }
            );
        }

        [Test]
        public void Providers_GetTransactions_For_Account_After()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    // Setup data
                    IAccount bankAccount1 = provider.CreateAccount(11, AccountType.ASSET);
                    IAccount creditorAccount2 = provider.CreateAccount(12, AccountType.LIABILITY);
                    IAccount creditorAccount3 = provider.CreateAccount(13, AccountType.LIABILITY);
                    ITransaction transaction = provider.CreateTransaction(DateTimeOffset.Now);
                    transaction.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount2, new Decimal(20.95)));
                    transaction.Commit();
                    ITransaction transaction2 = provider.CreateTransaction(DateTimeOffset.Now);
                    DateTimeOffset afterTransaction2 = DateTimeOffset.Now.AddMilliseconds(1);
                    transaction2.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction2.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount3, new Decimal(20.95)));
                    transaction2.Commit();
                    ITransaction transaction3 = provider.CreateTransaction(afterTransaction2.AddMilliseconds(1));
                    transaction3.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction3.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount3, new Decimal(20.95)));
                    transaction3.Commit();

                    List<ITransaction> transactions = new List<ITransaction>(provider.GetTransactions(creditorAccount3, afterTransaction2));
                    Assert.AreEqual(1, transactions.Count);
                    Assert.IsTrue(transactions.Exists(
                            delegate(ITransaction trans)
                            {
                                List<IAction> actions = new List<IAction>(trans);
                                List<IAction> accountActions = actions.FindAll(
                                    delegate(IAction action)
                                    {
                                        return action.Account.Number == creditorAccount3.Number;
                                    }
                                );
                                return accountActions.Count == 1;
                            }
                        )
                    );
                }
            );
        }

        [Test]
        public void Providers_GetTransactions_For_Account_Before()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    // Setup data
                    IAccount bankAccount1 = provider.CreateAccount(11, AccountType.ASSET);
                    IAccount creditorAccount2 = provider.CreateAccount(12, AccountType.LIABILITY);
                    IAccount creditorAccount3 = provider.CreateAccount(13, AccountType.LIABILITY);
                    ITransaction transaction = provider.CreateTransaction(DateTimeOffset.Now);
                    transaction.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount2, new Decimal(20.95)));
                    transaction.Commit();
                    ITransaction transaction2 = provider.CreateTransaction(DateTimeOffset.Now);
                    DateTimeOffset afterTransaction2 = DateTimeOffset.Now.AddMilliseconds(1);
                    transaction2.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction2.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount3, new Decimal(20.95)));
                    transaction2.Commit();
                    ITransaction transaction3 = provider.CreateTransaction(afterTransaction2.AddMilliseconds(1));
                    transaction3.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction3.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount3, new Decimal(20.95)));
                    transaction3.Commit();

                    List<ITransaction> transactions = new List<ITransaction>(provider.GetTransactions(bankAccount1, null, afterTransaction2));
                    Assert.AreEqual(2, transactions.Count);
                    Assert.AreEqual(2, transactions.FindAll(
                            delegate(ITransaction trans)
                            {
                                List<IAction> actions = new List<IAction>(trans);
                                List<IAction> accountActions = actions.FindAll(
                                    delegate(IAction action)
                                    {
                                        return action.Account.Number == bankAccount1.Number;
                                    }
                                );
                                return accountActions.Count == 1;
                            }
                        ).Count
                    );
                }
            );
        }

        [Test]
        public void Providers_GetTransactions_For_Account_After_And_Before()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    // Setup data
                    IAccount bankAccount1 = provider.CreateAccount(11, AccountType.ASSET);
                    IAccount creditorAccount2 = provider.CreateAccount(12, AccountType.LIABILITY);
                    IAccount creditorAccount3 = provider.CreateAccount(13, AccountType.LIABILITY);
                    DateTimeOffset startTime = DateTimeOffset.Now;
                    DateTimeOffset transaction1Time = startTime.AddMilliseconds(1);
                    ITransaction transaction = provider.CreateTransaction(transaction1Time);
                    transaction.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount2, new Decimal(20.95)));
                    transaction.Commit();
                    DateTimeOffset afterTransaction1 = transaction.Timestamp.AddMilliseconds(1);
                    DateTimeOffset transaction2Time = transaction1Time.AddMilliseconds(1);
                    ITransaction transaction2 = provider.CreateTransaction(transaction2Time);
                    transaction2.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction2.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount3, new Decimal(20.95)));
                    transaction2.Commit();
                    DateTimeOffset transaction3Time = transaction2Time.AddMilliseconds(1);
                    ITransaction transaction3 = provider.CreateTransaction(transaction3Time);
                    transaction3.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction3.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount3, new Decimal(20.95)));
                    transaction3.Commit();

                    List<ITransaction> transactions = new List<ITransaction>(provider.GetTransactions(bankAccount1, afterTransaction1, transaction3Time));
                    Assert.AreEqual(1, transactions.Count);
                    Assert.AreEqual(1, transactions.FindAll(
                            delegate(ITransaction trans)
                            {
                                List<IAction> actions = new List<IAction>(trans);
                                List<IAction> accountActions = actions.FindAll(
                                    delegate(IAction action)
                                    {
                                        return action.Account.Number == bankAccount1.Number;
                                    }
                                );
                                return accountActions.Count == 1;
                            }
                        ).Count
                    );
                }
            );
        }

        [Test]
        public void Providers_Transaction_Add_After_Commited()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    IAccount bankAccount1 = provider.CreateAccount(11, AccountType.ASSET);
                    IAccount creditorAccount2 = provider.CreateAccount(12, AccountType.LIABILITY);
                    ITransaction transaction = provider.CreateTransaction(DateTimeOffset.Now);
                    transaction.Add(provider.CreateAction(ActionType.DEBIT, bankAccount1, new Decimal(20.95)));
                    transaction.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount2, new Decimal(20.95)));
                    transaction.Commit();
                    try
                    {
                        transaction.Add(provider.CreateAction(ActionType.CREDIT, creditorAccount2, new Decimal(20.95)));
                        Assert.Fail("Expected exception because the action was added after the transaction was commited");
                    }
                    catch (Exception)
                    {

                    }
                }
            );
        }

        [Test]
        public void Providers_CreateContact()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    //if (provider.Name == "MSSQLDbProvider")
                    //{
                    //    Assert.Pass("Provider {0} does not yet implement CreateContact", provider.Name);
                    //}
                    IContact contact = provider.CreateContact();
                }
            );
        }

        [Test]
        public void Providers_GetContact()
        {
            ExecuteTest(
                delegate(MonengineProviderBase provider)
                {
                    IContact contact = provider.CreateContact();
                    contact.Email = "tester@4thex.com";
                    contact.Name.FamilyName = "Cogburn";
                    contact.Name.GivenName = "Palle";
                    contact.Name.NamePrefix = "Mr.";
                    contact.Name.NameSuffix = "";
                    contact.Name.FormattedName = "Mr. Palle Cogburn";
                    contact.Photo =  new ImageProducer(new Size(30, 30), Color.Blue).Image;

                    IContact contact2 = provider.GetContact(contact.ID);
                    Assert.AreEqual(contact.ID, contact2.ID, "ID is different");
                    Assert.AreEqual(contact.Email, contact2.Email, "Email is different");
                    Assert.AreEqual(contact.Name.FamilyName, contact2.Name.FamilyName, "Name.FamilyName is different");
                    Assert.AreEqual(contact.Name.GivenName, contact2.Name.GivenName, "Name.GivenName is different");
                    Assert.AreEqual(contact.Name.NamePrefix, contact2.Name.NamePrefix, "Name.NamePrefix is different");
                    Assert.AreEqual(contact.Name.NameSuffix, contact2.Name.NameSuffix, "Name.NameSuffix is different");
                    Assert.AreEqual(contact.Name.FormattedName, contact2.Name.FormattedName, "Name.FormattedName is different");
                    byte[] imageBytes;
                    byte[] imageReadBytes;
                    using (var stream1 = new MemoryStream())
                    {
                        contact.Photo.Save(stream1, ImageFormat.Png);
                        imageBytes = stream1.ToArray();
                    }
                    using (var stream2 = new MemoryStream())
                    {
                        contact2.Photo.Save(stream2, ImageFormat.Png);
                        imageReadBytes = stream2.ToArray();
                    }
                    Assert.AreEqual(imageBytes, imageReadBytes);
                }
            );
        }

        private class ImageProducer
        {
            Image image_;
            public ImageProducer(Size size, Color color)
            {
                image_ = new Bitmap(size.Width, size.Height);
                using (var g = Graphics.FromImage(image_))
                {
                    var pen = new Pen(color, size.Height / 10);
                    g.DrawLine(pen, new Point(0, 0), new Point(size.Width, size.Height));
                }
            }

            public Image Image
            {
                get
                {
                    return image_;
                }
            }
        }

        private void ExecuteTest(Action<MonengineProviderBase> action)
        {
            foreach(MonengineProviderBase provider in MonengineManager.Providers)
            {
                action.Invoke(provider);
                Console.WriteLine("Finished executing test using {0}", provider);
            }
        }
    }
}
